import { functionalUpdate } from './utils'
import type { AnyRouter } from './router'
import type { ParsedLocation } from './location'
import type { NonNullableUpdater } from './utils'
import type { HistoryLocation } from '@tanstack/history'

export type ScrollRestorationEntry = { scrollX: number; scrollY: number }

export type ScrollRestorationByElement = Record<string, ScrollRestorationEntry>

export type ScrollRestorationByKey = Record<string, ScrollRestorationByElement>

export type ScrollRestorationCache = {
  state: ScrollRestorationByKey
  set: (updater: NonNullableUpdater<ScrollRestorationByKey>) => void
}
export type ScrollRestorationOptions = {
  getKey?: (location: ParsedLocation) => string
  scrollBehavior?: ScrollToOptions['behavior']
}

function getSafeSessionStorage() {
  try {
    if (
      typeof window !== 'undefined' &&
      typeof window.sessionStorage === 'object'
    ) {
      return window.sessionStorage
    }
  } catch {
    // silent
  }
  return undefined
}

/** SessionStorage key used to persist scroll restoration state. */
/** SessionStorage key used to store scroll positions across navigations. */
/** SessionStorage key used to store scroll positions across navigations. */
export const storageKey = 'tsr-scroll-restoration-v1_3'

const throttle = (fn: (...args: Array<any>) => void, wait: number) => {
  let timeout: any
  return (...args: Array<any>) => {
    if (!timeout) {
      timeout = setTimeout(() => {
        fn(...args)
        timeout = null
      }, wait)
    }
  }
}

function createScrollRestorationCache(): ScrollRestorationCache | null {
  const safeSessionStorage = getSafeSessionStorage()
  if (!safeSessionStorage) {
    return null
  }

  const persistedState = safeSessionStorage.getItem(storageKey)
  let state: ScrollRestorationByKey = persistedState
    ? JSON.parse(persistedState)
    : {}

  return {
    state,
    // This setter is simply to make sure that we set the sessionStorage right
    // after the state is updated. It doesn't necessarily need to be a functional
    // update.
    set: (updater) => (
      (state = functionalUpdate(updater, state) || state),
      safeSessionStorage.setItem(storageKey, JSON.stringify(state))
    ),
  }
}

/** In-memory handle to the persisted scroll restoration cache. */
/** In-memory handle to the persisted scroll restoration cache. */
/** In-memory handle to the persisted scroll restoration cache. */
export const scrollRestorationCache = createScrollRestorationCache()

/**
 * The default `getKey` function for `useScrollRestoration`.
 * It returns the `key` from the location state or the `href` of the location.
 *
 * The `location.href` is used as a fallback to support the use case where the location state is not available like the initial render.
 */

/**
 * Default scroll restoration cache key: location state key or full href.
 */
/**
 * Default scroll restoration cache key: location state key or full href.
 */
/**
 * Default scroll restoration cache key: location state key or full href.
 */
export const defaultGetScrollRestorationKey = (location: ParsedLocation) => {
  return location.state.__TSR_key! || location.href
}

/** Best-effort nth-child CSS selector for a given element. */
export function getCssSelector(el: any): string {
  const path = []
  let parent: HTMLElement
  while ((parent = el.parentNode)) {
    path.push(
      `${el.tagName}:nth-child(${Array.prototype.indexOf.call(parent.children, el) + 1})`,
    )
    el = parent
  }
  return `${path.reverse().join(' > ')}`.toLowerCase()
}

let ignoreScroll = false

// NOTE: This function must remain pure and not use any outside variables
// unless they are passed in as arguments. Why? Because we need to be able to
// toString() it into a script tag to execute as early as possible in the browser
// during SSR. Additionally, we also call it from within the router lifecycle
/**
 * Restore scroll positions for window/elements based on cached entries.
 */
/**
 * Restore scroll positions for window/elements based on cached entries.
 */
export function restoreScroll({
  storageKey,
  key,
  behavior,
  shouldScrollRestoration,
  scrollToTopSelectors,
  location,
}: {
  storageKey: string
  key?: string
  behavior?: ScrollToOptions['behavior']
  shouldScrollRestoration?: boolean
  scrollToTopSelectors?: Array<string | (() => Element | null | undefined)>
  location?: HistoryLocation
}) {
  let byKey: ScrollRestorationByKey

  try {
    byKey = JSON.parse(sessionStorage.getItem(storageKey) || '{}')
  } catch (error) {
    console.error(error)
    return
  }

  const resolvedKey = key || window.history.state?.__TSR_key
  const elementEntries = byKey[resolvedKey]

  //
  ignoreScroll = true

  //
  scroll: {
    // If we have a cached entry for this location state,
    // we always need to prefer that over the hash scroll.
    if (
      shouldScrollRestoration &&
      elementEntries &&
      Object.keys(elementEntries).length > 0
    ) {
      for (const elementSelector in elementEntries) {
        const entry = elementEntries[elementSelector]!
        if (elementSelector === 'window') {
          window.scrollTo({
            top: entry.scrollY,
            left: entry.scrollX,
            behavior,
          })
        } else if (elementSelector) {
          const element = document.querySelector(elementSelector)
          if (element) {
            element.scrollLeft = entry.scrollX
            element.scrollTop = entry.scrollY
          }
        }
      }

      break scroll
    }

    // If we don't have a cached entry for the hash,
    // Which means we've never seen this location before,
    // we need to check if there is a hash in the URL.
    // If there is, we need to scroll it's ID into view.
    const hash = (location ?? window.location).hash.split('#', 2)[1]

    if (hash) {
      const hashScrollIntoViewOptions =
        window.history.state?.__hashScrollIntoViewOptions ?? true

      if (hashScrollIntoViewOptions) {
        const el = document.getElementById(hash)
        if (el) {
          el.scrollIntoView(hashScrollIntoViewOptions)
        }
      }

      break scroll
    }

    // If there is no cached entry for the hash and there is no hash in the URL,
    // we need to scroll to the top of the page for every scrollToTop element
    const scrollOptions = { top: 0, left: 0, behavior }
    window.scrollTo(scrollOptions)
    if (scrollToTopSelectors) {
      for (const selector of scrollToTopSelectors) {
        if (selector === 'window') continue
        const element =
          typeof selector === 'function'
            ? selector()
            : document.querySelector(selector)
        if (element) element.scrollTo(scrollOptions)
      }
    }
  }

  //
  ignoreScroll = false
}

/** Setup global listeners and hooks to support scroll restoration. */
/** Setup global listeners and hooks to support scroll restoration. */
export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
  if (!scrollRestorationCache && !router.isServer) {
    return
  }
  const shouldScrollRestoration =
    force ?? router.options.scrollRestoration ?? false

  if (shouldScrollRestoration) {
    router.isScrollRestoring = true
  }

  if (
    router.isServer ||
    router.isScrollRestorationSetup ||
    !scrollRestorationCache
  ) {
    return
  }

  router.isScrollRestorationSetup = true

  //
  ignoreScroll = false

  const getKey =
    router.options.getScrollRestorationKey || defaultGetScrollRestorationKey

  window.history.scrollRestoration = 'manual'

  // // Create a MutationObserver to monitor DOM changes
  // const mutationObserver = new MutationObserver(() => {
  //   ;ignoreScroll = true
  //   requestAnimationFrame(() => {
  //     ;ignoreScroll = false

  //     // Attempt to restore scroll position on each dom
  //     // mutation until the user scrolls. We do this
  //     // because dynamic content may come in at different
  //     // ticks after the initial render and we want to
  //     // keep up with that content as much as possible.
  //     // As soon as the user scrolls, we no longer need
  //     // to attempt router.
  //     // console.log('mutation observer restoreScroll')
  //     restoreScroll(
  //       storageKey,
  //       getKey(router.state.location),
  //       router.options.scrollRestorationBehavior,
  //     )
  //   })
  // })

  // const observeDom = () => {
  //   // Observe changes to the entire document
  //   mutationObserver.observe(document, {
  //     childList: true, // Detect added or removed child nodes
  //     subtree: true, // Monitor all descendants
  //     characterData: true, // Detect text content changes
  //   })
  // }

  // const unobserveDom = () => {
  //   mutationObserver.disconnect()
  // }

  // observeDom()

  const onScroll = (event: Event) => {
    // unobserveDom()

    if (ignoreScroll || !router.isScrollRestoring) {
      return
    }

    let elementSelector = ''

    if (event.target === document || event.target === window) {
      elementSelector = 'window'
    } else {
      const attrId = (event.target as Element).getAttribute(
        'data-scroll-restoration-id',
      )

      if (attrId) {
        elementSelector = `[data-scroll-restoration-id="${attrId}"]`
      } else {
        elementSelector = getCssSelector(event.target)
      }
    }

    const restoreKey = getKey(router.state.location)

    scrollRestorationCache.set((state) => {
      const keyEntry = (state[restoreKey] ||= {} as ScrollRestorationByElement)

      const elementEntry = (keyEntry[elementSelector] ||=
        {} as ScrollRestorationEntry)

      if (elementSelector === 'window') {
        elementEntry.scrollX = window.scrollX || 0
        elementEntry.scrollY = window.scrollY || 0
      } else if (elementSelector) {
        const element = document.querySelector(elementSelector)
        if (element) {
          elementEntry.scrollX = element.scrollLeft || 0
          elementEntry.scrollY = element.scrollTop || 0
        }
      }

      return state
    })
  }

  // Throttle the scroll event to avoid excessive updates
  if (typeof document !== 'undefined') {
    document.addEventListener('scroll', throttle(onScroll, 100), true)
  }

  router.subscribe('onRendered', (event) => {
    // unobserveDom()

    const cacheKey = getKey(event.toLocation)

    // If the user doesn't want to restore the scroll position,
    // we don't need to do anything.
    if (!router.resetNextScroll) {
      router.resetNextScroll = true
      return
    }
    if (typeof router.options.scrollRestoration === 'function') {
      const shouldRestore = router.options.scrollRestoration({
        location: router.latestLocation,
      })
      if (!shouldRestore) {
        return
      }
    }

    restoreScroll({
      storageKey,
      key: cacheKey,
      behavior: router.options.scrollRestorationBehavior,
      shouldScrollRestoration: router.isScrollRestoring,
      scrollToTopSelectors: router.options.scrollToTopSelectors,
      location: router.history.location,
    })

    if (router.isScrollRestoring) {
      // Mark the location as having been seen
      scrollRestorationCache.set((state) => {
        state[cacheKey] ||= {} as ScrollRestorationByElement

        return state
      })
    }
  })
}

/**
 * @private
 * Handles hash-based scrolling after navigation completes.
 * To be used in framework-specific <Transitioner> components during the onResolved event.
 *
 * Provides hash scrolling for programmatic navigation when default browser handling is prevented.
 * @param router The router instance containing current location and state
 */
/**
 * @private
 * Handles hash-based scrolling after navigation completes.
 * To be used in framework-specific <Transitioner> components during the onResolved event.
 *
 * Provides hash scrolling for programmatic navigation when default browser handling is prevented.
 * @param router The router instance containing current location and state
 */
/**
 * @private
 * Handles hash-based scrolling after navigation completes.
 * To be used in framework-specific Transitioners.
 */
export function handleHashScroll(router: AnyRouter) {
  if (typeof document !== 'undefined' && (document as any).querySelector) {
    const hashScrollIntoViewOptions =
      router.state.location.state.__hashScrollIntoViewOptions ?? true

    if (hashScrollIntoViewOptions && router.state.location.hash !== '') {
      const el = document.getElementById(router.state.location.hash)
      if (el) {
        el.scrollIntoView(hashScrollIntoViewOptions)
      }
    }
  }
}
